Skip to content

feat: add --dry-run flag to mutating commands#387

Merged
BYK merged 28 commits intomainfrom
feat/dry-run
Mar 11, 2026
Merged

feat: add --dry-run flag to mutating commands#387
BYK merged 28 commits intomainfrom
feat/dry-run

Conversation

@BYK
Copy link
Member

@BYK BYK commented Mar 10, 2026

Summary

Add --dry-run flag to sentry api and sentry project create so agents and users can preview what would happen without executing mutating operations.

Fixes #349

Changes

sentry api --dry-run

Shows the fully resolved request without sending it:

$ sentry api /organizations/my-org/issues/ -X POST -d '{"status":"resolved"}' --dry-run

Dry run — no request sent.

  Method:   POST
  URL:      https://sentry.io/api/0/organizations/my-org/issues/?...
  Headers:  Content-Type: application/json
  Body:     {"status": "resolved"}
  • Validates endpoint normalization, method, body construction, headers
  • Supports --json for machine-readable output (--fields for filtering)
  • Short alias: -n

sentry project create --dry-run

Validates inputs and shows what would be created:

$ sentry project create my-app node --dry-run

Dry run — no project created.

  Organization:  acme-corp
  Team:          engineering (auto-selected)
  Name:          my-app
  Slug:          my-app
  Platform:      node
  • Still validates platform and resolves org/team
  • Does NOT call createProject or fetch DSN
  • Supports --json

Also

  • Added output: "json" to the api command for --json/--fields support

Testing

  • 18 unit tests for resolveRequestUrl, buildDryRunRequest, writeDryRunHuman
  • 8 property-based tests for URL construction and request building invariants
  • 5 tests for project create dry-run (no API calls, platform validation, JSON output)
  • All 299 affected tests pass, typecheck and lint clean

Preview what would happen without executing mutating operations.

## sentry api --dry-run

Shows the fully resolved request without sending it:
- Method, full URL (with resolved base + query params)
- Headers and body
- Supports --json for machine-readable output
- Short alias: -n

## sentry project create --dry-run

Validates inputs and shows what would be created:
- Organization (resolved from args/config/DSN)
- Team (auto-selected/created as normal)
- Name, slug, platform
- Does NOT call createProject or fetch DSN
- Still validates platform and resolves org/team

Also adds output: "json" to the api command for --json/--fields
support (previously had no structured output mode).

Fixes #349
@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Init

  • Enforce canonical feature display order by betegon in #388
  • Accept multiple delimiter formats for --features flag by betegon in #386
  • Add git safety checks before wizard modifies files by betegon in #379
  • Add experimental warning before wizard runs by betegon in #378
  • Add init command for guided Sentry project setup by betegon in #283

Other

  • (issue-list) Redesign table to match Sentry web UI by BYK in #372
  • Add --dry-run flag to mutating commands by BYK in #387
  • Return-based output with OutputConfig on buildCommand by BYK in #380
  • Add --fields flag for context-window-friendly JSON output by BYK in #373
  • Magic @ selectors (@latest, @most_frequent) for issue commands by BYK in #371
  • Input hardening against agent hallucinations by BYK in #370
  • Add response caching for read-only API calls by BYK in #330

Bug Fixes 🐛

Init

  • Remove implementation detail from help text by betegon in #385
  • Truncate uncommitted file list to first 5 entries by MathurAditya724 in #381

Other

  • (api) Convert --data to query params for GET requests by BYK in #383
  • (docs) Remove double borders and fix column alignment on landing page tables by betegon in #369
  • Add trace ID validation to trace view + UUID dash-stripping by BYK in #375

Internal Changes 🔧

Init

  • Remove --force flag by betegon in #377
  • Remove dead determine-pm step label by betegon in #374

Other

  • Convert remaining Tier 1 commands to return-based output by BYK in #382
  • Converge Tier 1 commands to writeOutput helper by BYK in #376

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 10, 2026

Codecov Results 📊

104 passed | Total: 104 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 99.26%. Project has 900 uncovered lines.
✅ Project coverage is 95.5%. Comparing base (base) to head (head).

Files with missing lines (2)
File Patch % Lines
human.ts 97.15% ⚠️ 28 Missing
command.ts 99.35% ⚠️ 1 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    95.49%    95.50%    +0.01%
==========================================
  Files          142       142         —
  Lines        19954     19999       +45
  Branches         0         0         —
==========================================
+ Hits         19055     19099       +44
- Misses         899       900        +1
- Partials         0         0         —

Generated by Codecov Action

@BYK BYK marked this pull request as ready for review March 10, 2026 19:41
- Prevent team auto-creation side effect in dry-run mode by passing
  autoCreateSlug: undefined when --dry-run is set (BugBot HIGH)
- Use getDefaultSdkConfig().baseUrl in resolveRequestUrl to match
  rawApiRequest URL normalization, preventing double-slash URLs
  when SENTRY_URL has trailing slash (BugBot MEDIUM)
Continuation lines of JSON.stringify output are now indented to
align with the first line after the 'Body:     ' label prefix.

Also uses getDefaultSdkConfig().baseUrl (with trailing-slash
normalization) instead of raw getApiBaseUrl() in resolveRequestUrl
to match the URL rawApiRequest would actually use.
BYK and others added 6 commits March 10, 2026 20:28
When an org has no teams, normal mode auto-creates a team. Dry-run
now reflects this by returning { slug, source: 'auto-created' }
without calling the createTeam API.

Previously dry-run passed autoCreateSlug: undefined which caused a
ContextError. Now passes dryRun: true to resolveOrCreateTeam which
skips the mutation while preserving the preview.
The api command is a raw API proxy — the response is already JSON
from the Sentry API. Having output: 'json' from buildCommand injected
--json and --fields flags that only worked in --dry-run mode, which
was confusing.

Now --json is an explicit flag documented as applying to dry-run
preview output only. Removed --fields since it doesn't apply.
--fields is useful for filtering both the raw API response and dry-run
output. Restored output: 'json' on apiCommand so buildCommand injects
--json/--fields flags. handleResponse now applies --fields filtering
to the response body when provided.
Replace manual flags.json/writeJson branching with writeOutput(),
which is the shared utility that handles --json/--fields/human
formatting. Extract formatDryRun as a pure function for the
human-readable dry-run preview.
Mirror rawApiRequest behavior: when body is an object and no
Content-Type header was explicitly provided, auto-add
Content-Type: application/json in buildDryRunRequest.
BYK added 5 commits March 10, 2026 21:40
Biome's auto-fix placed the type and function between import
statements. Move them after all imports to maintain standard
module structure.
Standardize dry-run human output to use the same markdown KV table
pattern as formatProjectCreated. Uses ## heading, > blockquote notes
for team source, and mdKvTable for key-value data. Removes custom
manual alignment formatting.

Also fixes interleaved imports: DryRunData type and formatDryRun
function are now placed after all import statements.
Both paths now construct a ProjectCreatedResult and return { data }
through buildCommand's output wrapper. The single formatProjectCreated
formatter handles both modes — dry-run adds a dryRun flag that adjusts
the heading and team source note wording.

Removes DryRunData type, formatDryRun function, and writeOutput import.
Net -32 lines.
- Upgrade output: 'json' → { json: true, human: formatDryRunRequest }
  so dry-run returns { data } like project create (normal path returns
  void, which the wrapper silently ignores)
- Convert writeDryRunHuman to formatDryRunRequest: pure function
  returning rendered markdown using mdKvTable (same pattern as other
  commands), replaces imperative stdout.write calls
- Simplify buildDryRunRequest signature: (method, endpoint, options)
  instead of single object — mirrors how rawApiRequest is called
- Remove muted import (now uses <muted> color tag in markdown)
The api command is a JSON proxy — its output should always be JSON,
not conditionally switch to human-readable markdown in dry-run mode.

- Revert output config to 'json' (flag-only, no human formatter)
- Dry-run writes JSON directly via writeJson (imperative, since
  flag-only mode doesn't intercept returns)
- Remove formatDryRunRequest and its markdown imports entirely
- Remove formatDryRunRequest test suite (6 tests)
BYK added 2 commits March 11, 2026 11:10
…uest

Both code paths now return { data } through the standard output system:
- Dry-run: returns { data: { method, url, headers, body } } preview
- Normal: returns { data: response.body } after side-effect writes
  (verbose/include headers, error exit code)

Key changes:
- output: 'json' → { json: true } (JSON-only config, no human formatter)
- Make OutputConfig.human optional — when absent, renderCommandOutput
  always serializes as JSON regardless of --json flag
- Remove buildDryRunRequest — inline request preview construction using
  resolveRequestUrl + new resolveEffectiveHeaders helper
- Remove handleResponse — its behaviors are inlined in func:
  verbose/include headers as pre-return side effects, silent mode
  returns void, error uses process.exitCode (not process.exit) so
  the output wrapper can render before the process exits
- Remove DryRunRequest type, writeJson import

Tests: replace handleResponse tests (8) and buildDryRunRequest tests
(4+5 property) with resolveEffectiveHeaders tests (6+4 property).
288 tests pass across 3 files.
BYK added 2 commits March 11, 2026 11:19
Stricli overwrites process.exitCode after the command returns,
so process.exitCode = 1 was silently reset to 0. For error
responses, write JSON directly then process.exit(1). Success
responses still use return-based { data } through the output
system.
… divergence

Address three bot review comments:

1. String response quoting regression (Seer + BugBot): the api command
   is a raw proxy — non-JSON responses (plain text, HTML error pages)
   must not be wrapped in JSON quotes. Revert to output: 'json'
   (flag-only) and write response body imperatively via writeResponseBody,
   which writes strings directly and JSON-formats objects with --fields.

2. Null body header divergence (BugBot): resolveEffectiveHeaders had an
   extra body !== null check that rawApiRequest doesn't. For --data
   "null", dry-run would omit Content-Type while the real request adds
   it. Aligned condition to match rawApiRequest exactly:
   !(isStringBody || hasContentType) && body !== undefined.

3. Stale comment about dry-run always outputting JSON (BugBot on old
   commit 70e7f2d): the referenced code was completely rewritten in
   6980508. No action needed — will reply to dismiss.

Also reverts OutputConfig.human back to required (the JSON-only config
form is no longer needed since the api command doesn't use it).
@BYK
Copy link
Member Author

BYK commented Mar 11, 2026

Re: Seer finding on dry-run human format (line 1091):

This is intentional. The api command is a raw API proxy — its formatApiResponse human formatter JSON-pretty-prints objects and passes strings through raw. For dry-run, the preview object {method, url, headers, body} gets pretty-printed, which is exactly the same behavior as gh api's dry-run. A fancier curl-style format would be a nice-to-have but is out of scope for this PR.

@BYK BYK force-pushed the feat/dry-run branch 2 times, most recently from c06fe6f to 8d4aebf Compare March 11, 2026 12:00
…ttern

Both dry-run and normal response paths now return { data } through the
output system. No more imperative writeJson/writeResponseBody calls.

Changes:
- Add OutputError class (extends CliError) for commands that produce
  valid output but should exit non-zero. The buildCommand wrapper
  catches it, renders data through the output system, then calls
  process.exit(). This replaces the muddled exitCode field on
  CommandOutput<T>.
- Add formatApiResponse human formatter — preserves raw strings (plain
  text, HTML error pages) without JSON quoting, JSON-formats objects.
  Does NOT add trailing newline (renderCommandOutput appends it).
- Api command uses output: { json: true, human: formatApiResponse }
  instead of flag-only output: "json". Error responses throw
  OutputError(response.body) instead of returning { exitCode: 1 }.
- Remove writeResponseBody function and writeJson import from api.ts.
- Separate stdout/stderr in test context to prevent Stricli error
  messages from polluting JSON output assertions.
Replace imperative writeVerboseRequest, writeVerboseResponse, and
writeResponseHeaders functions with return-based output:

- Default: return raw response.body
- --include: return { status, headers, body } envelope
- --verbose: return { request, status, headers, body } full envelope

The human formatter (formatApiResponse) handles all three shapes:
- Raw body → formatBody (JSON-format objects, passthrough strings)
- --include envelope → HTTP status + headers + body
- --verbose envelope → '> ' request block + '< ' response block + body

JSON mode gets the same structured data — --include/--verbose now
produce structured JSON output instead of mixed text.

Also: --silent + error uses OutputError(null) instead of bare
process.exit(1), keeping the framework in control of exit codes.

Delete dead functions: writeResponseHeaders, writeVerboseRequest,
writeVerboseResponse.
BYK and others added 2 commits March 11, 2026 13:21
--include wraps the body in { status, headers, body } which breaks
--fields filtering (users would need --fields body.name instead of
--fields name). Remove it entirely.

--verbose request/response metadata is diagnostic info that belongs
on stderr, not in the return data. Move to logger.debug() calls via
consola — the framework already sets log level to debug when
--verbose is passed, so the log lines appear automatically.

The api command now always returns raw response.body. No envelopes,
no shape changes per flags. --fields works directly on API response
fields.

Deleted: ApiResponseEnvelope type, isEnvelope() guard, formatBody()
(renamed back to formatApiResponse), envelope rendering logic,
headersToObject helper, --include flag + -i alias.
…utput bug

- Delete E2E tests for removed --include/-i flag
- Fix --verbose E2E test: check stderr (logger.debug) not stdout
- Fix renderCommandOutput null data bug: skip rendering when
  OutputError.data is null/undefined instead of writing spurious \n
- Update api docs: replace --include example with --verbose section
- Regenerate SKILL.md to remove stale --include references
@github-actions
Copy link
Contributor

github-actions bot commented Mar 11, 2026

PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/pr-preview/pr-387/

Built to branch gh-pages at 2026-03-11 14:56 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

Consola's BasicReporter (used when stderr is piped, e.g. in CI or
Bun.spawn) routes debug and info messages to stdout instead of stderr.
This contaminated command output when --verbose was used in non-TTY
contexts.

Fix: set stdout: process.stderr in createConsola() so ALL diagnostic
log output goes to stderr regardless of reporter or TTY mode.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@BYK
Copy link
Member Author

BYK commented Mar 11, 2026

Re: BugBot finding on --silent + --verbose interaction:

Fixed both issues:

  1. --verbose + --silent leak: Added !flags.silent guard — verbose flag resolved as const verbose = flags.verbose && !flags.silent. Silent suppresses all output including diagnostics.

  2. Complexity: Extracted logRequest() and logResponse() helpers (curl-verbose style) to bring cognitive complexity within limits.

The OutputError without outputConfig concern is theoretical — the api command always has output: { json: true, ... } configured.

- Guard verbose logging with !flags.silent to prevent leak when
  --verbose and --silent are combined (BugBot finding)
- Extract logRequest() and logResponse() helpers to reduce func
  cognitive complexity from 17 to within limit
@BYK BYK merged commit fc90123 into main Mar 11, 2026
22 checks passed
@BYK BYK deleted the feat/dry-run branch March 11, 2026 15:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add --dry-run flag to mutating commands (sentry api, project create)

1 participant